﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Windows.Media;
using AgFractal.Coloring;

namespace AgFractal.Fractals
{
    internal sealed class FractalManager
    {
        private short[,] fractalMap = new short[2500, 2000];
        private double sx, sy;
        internal HistoryEntry LastEntry { get; private set; }
        private Func<double, double, short, short> func;
        internal int imageWidth, imageHeight;
        internal PngEncoder img;

        internal PngEncoder CalcFractal(HistoryEntry f, int w, int h)
        {
            return CalcFractal(f, w, h, true);
        }

        PngEncoder CalcFractal(HistoryEntry entry, int w, int h, bool fill)
        {
            LastEntry = entry;
            func = entry.Fractal.Function;
            imageWidth = w;
            imageHeight = h;
            img = new PngEncoder(imageWidth, imageHeight);
            if (fill)
            {
                for (int i = 0; i < imageWidth; i++)
                    for (int j = 0; j < imageHeight; j++)
                        fractalMap[j, i] = -1;
            }
            sx = (LastEntry.R2 - LastEntry.R1) / (imageWidth - 1);
            sy = (LastEntry.I2 - LastEntry.I1) / (imageHeight - 1);
            var th = new Thread(CalcPartition);
            var ti = new Thread(CalcPartition);
            th.Start(0);
            ti.Start(imageWidth / 18);
            ti.Join();
            th.Join();
            return img;
        }

        void CalcPartition(object p)
        {
            var startCol = (int)p;
            for (int x = startCol; x < startCol + imageWidth / 18; x++)
                for (int y = 0; y < imageHeight / 9; y++)
                    CalcRect(x * 9, y * 9, x * 9 + 8, y * 9 + 8);

            startCol = startCol * 9;
            var maxX = startCol + (imageWidth / 18) * 9;
            var maxY = (imageHeight / 9) * 9;
            for (int y = 0; y < maxY; y++)
            {
                var rowStart = img.GetRowStart(y);
                for (int x = startCol; x < maxX; x++)
                {
                    short idx = fractalMap[y, x];
                    Color c;
                    if (idx < MaxIteration)
                        c = coloring.GetColor(new Complex(), idx);
                    else
                        c = coloring2.GetColor(new Complex(), idx);

                    img.SetPixelAtRowStart(x, rowStart, c.R, c.G, c.B, c.A);
                }
            }
        }

        internal void ReApplyColoring()
        {
            var th = new Thread(ColorizePartition);
            var ti = new Thread(ColorizePartition);
            th.Start(0);
            ti.Start(imageWidth / 18);
            ti.Join();
            th.Join();
        }

        void ColorizePartition(object p)
        {
            var startCol = (int)p;

            startCol = startCol * 9;
            var maxX = startCol + (imageWidth / 18) * 9;
            var maxY = (imageHeight / 9) * 9;
            for (int y = 0; y < maxY; y++)
            {
                var rowStart = img.GetRowStart(y);
                for (int x = startCol; x < maxX; x++)
                {
                    short idx = fractalMap[y, x];
                    Color c;
                    if (idx < MaxIteration)
                        c = coloring.GetColor(new Complex(), idx);
                    else
                        c = coloring2.GetColor(new Complex(), idx);

                    img.SetPixelAtRowStart(x, rowStart, c.R, c.G, c.B, c.A);
                }
            }
        }



        double cx(double x) { return LastEntry.R1 + sx * x; }
        double cy(double y) { return LastEntry.I1 + sy * y; }

        void CalcRect(int x, int y, int x2, int y2)
        {
            short j, k, l, i = fractalMap[y, x];
            if (i < 0)
                fractalMap[y, x] = i = func(cx(x), cy(y), MaxIteration);

            j = fractalMap[y, x2]; if (j < 0) { fractalMap[y, x2] = j = func(cx(x2), cy(y), MaxIteration); }
            k = fractalMap[y2, x]; if (k < 0) { fractalMap[y2, x] = k = func(cx(x), cy(y2), MaxIteration); }
            l = fractalMap[y2, x2]; if (l < 0) { fractalMap[y2, x2] = l = func(cx(x2), cy(y2), MaxIteration); }
            if (i == j && j == k && k == l)
            {
                for (int a = x; a <= x2; a++)
                    for (int b = y; b <= y2; b++)
                    {
                        fractalMap[b, a] = i;
                    }
                return;
            }
            if (x + 1 < x2 || y + 1 < y2)
            {
                CalcRect(x, y, (x + x2) / 2, (y + y2) / 2);
                CalcRect(x, (y + y2) / 2, (x + x2) / 2, y2);
                CalcRect((x + x2) / 2, y, x2, (y + y2) / 2);
                CalcRect((x + x2) / 2, (y + y2) / 2, x2, y2);
            }
        }

        internal static List<IFractal> Fractals { get; private set; }
        internal short MaxIteration { get; set; }

        internal static readonly FractalManager Instance = new FractalManager();
        internal static readonly FractalManager Instance2 = new FractalManager();

        internal IColoring coloring;
        private IColoring coloring2;
        internal Gradient Gradient;
        private FractalManager()
        {
            MaxIteration = 250;
            Gradient = new Gradient();
            Gradient.Entries.Add(new ColorEntry { Idx = 0, Color = Colors.Black });
            Gradient.Entries.Add(new ColorEntry { Idx = 80, Color = Color.FromArgb(255, 0, 0, 255) });
            Gradient.Entries.Add(new ColorEntry { Idx = 160, Color = Color.FromArgb(255, 255, 0, 0) });
            Gradient.Entries.Add(new ColorEntry { Idx = 240, Color = Color.FromArgb(255, 0, 255, 0) });
            Gradient.Entries.Add(new ColorEntry { Idx = 320, Color = Color.FromArgb(255, 255, 255, 0) });
            Gradient.Entries.Add(new ColorEntry { Idx = 399, Color = Color.FromArgb(255, 0, 0, 0) });
            Gradient.CalcColors();
            coloring = new BasicColoring { Gradient = Gradient };
            coloring2 = new SolidColorColoring();
        }

        static FractalManager()
        {
            Fractals = new List<IFractal>();
            var ft = typeof(IFractal);
            foreach (var t in Assembly.GetExecutingAssembly().GetTypes())
            {
                if (ft.IsAssignableFrom(t) && !t.IsAbstract)
                {
                    IFractal fractal = (IFractal)Activator.CreateInstance(t);
                    Fractals.Add(fractal);
                }
            }
        }

        internal PngEncoder Pan(int panX, int panY)
        {
            if (panY > 0)
            {
                for (int x = 0; x < imageWidth; x++)
                {
                    int y;
                    for (y = imageHeight; y >= panY; y--)
                        fractalMap[y, x] = fractalMap[y - panY, x];

                    for (y = 0; y < panY; y++)
                        fractalMap[y, x] = -1;
                }
            }
            else
            {
                int yMove = -panY;
                for (int x = 0; x < imageWidth; x++)
                {
                    int y;
                    for (y = 0; y < imageHeight - yMove; y++)
                        fractalMap[y, x] = fractalMap[y + yMove, x];

                    for (y = imageHeight - yMove; y < imageHeight; y++)
                        fractalMap[y, x] = -1;
                }
            }

            if (panX > 0)
            {
                for (int y = 0; y < imageHeight; y++)
                {
                    for (int x = imageWidth; x >= panX; x--)
                        fractalMap[y, x] = fractalMap[y, x - panX];

                    for (int x = 0; x < panX; x++)
                        fractalMap[y, x] = -1;
                }
            }
            else
            {
                int xMove = -panX;
                for (int y = 0; y < imageHeight; y++)
                {
                    for (int x = 0; x < imageWidth - xMove; x++)
                        fractalMap[y, x] = fractalMap[y, x + xMove];

                    for (int x = imageWidth - xMove; x < imageWidth; x++)
                        fractalMap[y, x] = -1;
                }
            }
            LastEntry.R1 = LastEntry.R1 - panX * sx;
            LastEntry.I1 = LastEntry.I1 - panY * sy;

            LastEntry.R2 = LastEntry.R2 - panX * sx;
            LastEntry.I2 = LastEntry.I2 - panY * sy;
            return CalcFractal(LastEntry, imageWidth, imageHeight, false);
        }

        internal PngEncoder ZoomOut(int zoomCenterX, int zoomCenterY)
        {
            return Zoom(zoomCenterX, zoomCenterY, 2);
        }

        internal PngEncoder ZoomIn(int zoomCenterX, int zoomCenterY)
        {
            return Zoom(zoomCenterX, zoomCenterY, 0.5m);
        }

        internal PngEncoder Zoom(int zoomCenterX, int zoomCenterY, decimal zoomFactor)
        {
            double cr = LastEntry.R1 + zoomCenterX * sx;
            double ci = LastEntry.I1 + zoomCenterY * sy;
            sx = sx / (double)zoomFactor;
            sy = sy / (double)zoomFactor;

            LastEntry.R1 = cr - sx * zoomCenterX;
            LastEntry.R2 = cr + sx * (imageWidth - zoomCenterX);

            LastEntry.I1 = ci - sy * zoomCenterY;
            LastEntry.I2 = ci + sy * (imageHeight - zoomCenterY);

            return CalcFractal(LastEntry, imageWidth, imageHeight, true);
        }

        internal PngEncoder CalcFractal(IFractal fractal, int w, int h)
        {
            var entry = new HistoryEntry(fractal)
            {
                I1 = fractal.I1,
                I2 = fractal.I2,
                R1 = fractal.R1,
                R2 = fractal.R2
            };
            return CalcFractal(entry, w, h, true);
        }
    }
}